<template>
  <div
    ref="el"
    class="fa-dnr"
    :class="{
      'is-draggable': draggable,
      'is-resizable': resizable,
      'is-dragging': move.isMoving,
      'is-resizing': resize.isMoving,
      [activeClass]: state.active
    }"
    :style="style"
    @mousedown.left="draggable && move.dragStart($event)"
    @touchstart="draggable && move.dragStart($event)"
    @click="onClick"
  >
    <slot />

    <div v-if="resizable && state.active" class="fa-dnr-handles">
      <div
        v-for="handle in handleList"
        :key="handle"
        class="handle"
        :class="`handle-${handle}`"
        @mousedown.left.stop="resize.dragStart($event, handle)"
        @touchstart="resize.dragStart($event, handle)"
      ></div>
    </div>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, PropType, ref, toRefs, watch } from 'vue'
import {
  DragAxis,
  DragBox,
  DragBoxDefault,
  DragElement,
  ResizeHandle,
  ResizeHandles,
  MiddleHandles,
  MovingData,
  ResizeData
} from './type'
import { EffectLock } from '@/utils'
import useMove from './useMove'
import useResize from './useResize'
import useState from './useState'

/** 拖拽位置和调整大小控件 */
export default defineComponent({
  name: 'FaDragResize',

  props: {
    /** 盒子选项 */
    data: {
      type: Object as PropType<Partial<DragBox>>,
      default: () => ({
        ...DragBoxDefault
      })
    },

    /** 是否允许拖拽移动位置 */
    draggable: {
      type: Boolean,
      default: true
    },

    /** 拖拽开始，返回 `false` 则阻止操作 */
    onDragStart: Function as PropType<(e: MouseEvent) => void | false>,

    /** 拖拽中，可拦截更改 `move` 数据  */
    onDragging: Function as PropType<(e: MouseEvent, move: MovingData) => void>,

    /** 是否允许调整大小 */
    resizable: {
      type: Boolean,
      default: false
    },

    /** 调整大小开始，返回 `false` 则阻止操作 */
    onResizeStart: Function as PropType<(e: MouseEvent) => void | false>,

    /** 调整大小中，可拦截更改 `resize` 数据 */
    onResizing: Function as PropType<(e: MouseEvent, resize: ResizeData) => void>,

    /** 激活时的类名 */
    activeClass: {
      type: String,
      default: 'is-active'
    },

    /** 是否限定移动范围为父元素，如果为 string 则指定 parent 节点 */
    parent: {
      type: [Boolean, String],
      default: true
    },

    /** 是否限定调整大小范围为父元素 */
    resizeParent: {
      type: Boolean,
      default: true
    },

    /** 是否以中心点位置调整大小 */
    resizeAtCenter: {
      type: Boolean,
      default: false
    },

    /** 拖拽坐标轴 */
    axis: {
      type: String as PropType<DragAxis>,
      default: DragAxis.BOTH
    },

    /** 调整大小句柄 */
    handles: {
      type: Array as PropType<ResizeHandle[]>,
      default: () => ['tl', 'tc', 'tr', 'lm', 'rm', 'br', 'bc', 'bl']
    },

    /** 调整大小时的宽高比例，为 0 则不设置 */
    aspectRatio: {
      type: Number,
      default: 0
    }
  },

  /** 事件 */
  emits: [
    /** 拖拽开始 `(e: MouseEvent) => void` */
    'drag-start',
    /** 拖拽移动中 `(e: MouseEvent, movingData: MovingData) => void` */
    'dragging',
    /** 拖拽结束 `(e: MouseEvent, data: DragState) => void` */
    'drag-stop',
    /** 调整大小开始 `(e: MouseEvent) => void` */
    'resize-start',
    /** 调整大小中 `(e: MouseEvent, resizeData: ResizeData) => void` */
    'resizing',
    /** 调整大小结束 `(e: MouseEvent, data: DragState) => void` */
    'resize-stop',
    /** 已激活 */
    'activated',
    /** 激活失效 */
    'deactivated',
    /**
     * 点击事件 `(e: MouseEvent) => void`
     * @description 避免使用原生 click 在拖拽结束后也会触发
     */
    'click'
  ],

  setup(props, { emit }) {
    const { data, parent, resizeParent, resizeAtCenter, axis, aspectRatio } = toRefs(props)

    /** 节点 */
    const el = ref(null as DragElement | null)

    /** 盒子数据 */
    const { box, state } = useState(data, el, parent, resizeParent, resizeAtCenter)

    /** 句柄，从 props 获取校验并去重 */
    const handles = computed(() => {
      const list = Array.from(new Set(props.handles.filter(item => ResizeHandles.includes(item))))

      // 等比调整大小时，忽略中间的句柄
      if (props.aspectRatio) {
        return list.filter(item => !MiddleHandles.includes(item))
      }

      return list
    })

    /**
     * 拖拽调整大小锁定
     * 拖拽结束后则锁定一段时间不触发 click
     */
    const dnsLocker = new EffectLock(50)

    /** 点击锁定，避免重复触发锁定 */
    const clickLocker = new EffectLock(50)

    /** 移动处理 */
    const move = useMove({
      box,
      state,

      onStart: e => {
        const willMove = props.onDragStart?.(e) !== false

        willMove && emit('drag-start', e)
      },

      onMoving(e, movingData) {
        props.onDragging?.(e, movingData)

        const { x, y } = movingData

        setBox(x, y)

        emit('dragging', e, movingData)
      },

      onEnd(e, data) {
        dnsLocker.lock()
        emit('drag-stop', e, data)
      }
    })

    /** 大小调整处理 */
    const resize = useResize({
      box,
      state,
      aspectRatio,

      onStart: e => {
        const willMove = props.onResizeStart?.(e) !== false

        willMove && emit('resize-start', e)
      },

      onResizing(e, resizeData) {
        props.onResizing?.(e, resizeData)

        const { x, y, w, h } = resizeData

        setBox(x, y, w, h)

        emit('resizing', e, resizeData)
      },

      onEnd(e, data) {
        dnsLocker.lock()
        emit('resize-stop', e, data)
      }
    })

    /** 样式 */
    const style = computed(() => {
      let transform = `translate(${box.x}px, ${box.y}px) `

      // 缩放
      if (box.scale !== DragBoxDefault.scale) {
        transform += `scale(${Array.isArray(box.scale) ? box.scale.join(',') : box.scale})`
      }

      return {
        width: typeof box.w === 'number' ? `${box.w}px` : box.w,
        height: typeof box.h === 'number' ? `${box.h}px` : box.h,
        transform
      }
    })

    /** 设置盒子 */
    function setBox(x: number = box.x, y: number = box.y, w: number | string = box.w, h: number | string = box.h) {
      Object.assign(box, {
        x: axis.value === DragAxis.Y ? box.x : x,
        y: axis.value === DragAxis.X ? box.y : y,
        w,
        h
      })
    }

    /** 激活 */
    watch(
      () => state.active,
      active => {
        emit(active ? 'activated' : 'deactivated')
      }
    )

    return {
      /** 节点 */
      el,

      /** 状态 */
      state,

      /** 句柄 */
      handleList: handles,

      /** 移动处理 */
      move,

      /** 大小调整处理 */
      resize,

      /** 样式 */
      style,

      /** 点击事件 */
      onClick(e: Event) {
        if (!dnsLocker.locked && !clickLocker.locked) {
          clickLocker.lock()
          emit('click', e)
        }
      }
    }
  }
})
</script>

<style lang="scss">
.fa-dnr {
  position: relative;

  &.is-draggable {
    cursor: move;
  }
}

// 句柄
.fa-dnr-handles {
  $size: 5px;
  $space: 2px;

  .handle {
    position: absolute;
    width: $size;
    height: $size;
    font-size: 0;
    background: #333;
  }

  // 上方
  .handle-tl,
  .handle-tc,
  .handle-tr {
    top: 0;
    margin-top: -$space;
  }

  // 下方
  .handle-bl,
  .handle-bc,
  .handle-br {
    bottom: 0;
    margin-bottom: -$space;
  }

  // 左方
  .handle-tl,
  .handle-bl,
  .handle-lm {
    left: 0;
    margin-left: -$space;
  }

  // 右方
  .handle-tr,
  .handle-br,
  .handle-rm {
    right: 0;
    margin-right: -$space;
  }

  // 水平居中
  .handle-tc,
  .handle-bc {
    left: 50%;
    margin-left: -$space;
    cursor: s-resize;
  }

  // 垂直居中
  .handle-lm,
  .handle-rm {
    top: 50%;
    margin-top: -$space;
    cursor: w-resize;
  }

  // 上左
  .handle-tl {
    cursor: nw-resize;
  }

  // 上右
  .handle-tr {
    cursor: ne-resize;
  }

  // 下左
  .handle-bl {
    cursor: sw-resize;
  }

  // 下右
  .handle-br {
    cursor: se-resize;
  }
}
</style>
